package io.quarkus.deployment.pkg.steps;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.SystemUtils;
import org.jboss.logging.Logger;

import io.smallrye.common.process.ProcessBuilder;
import io.smallrye.common.process.ProcessUtil;

public abstract class NativeImageBuildRunner {

    private static final Logger log = Logger.getLogger(NativeImageBuildRunner.class);

    private static GraalVM.Version graalVMVersion = null;

    public GraalVM.Version getGraalVMVersion() {
        if (graalVMVersion == null) {
            final String[] versionCommand = getGraalVMVersionCommand(List.of("--version"));
            try {
                graalVMVersion = ProcessBuilder.newBuilder(versionCommand[0])
                        .arguments(Arrays.copyOfRange(versionCommand, 1, versionCommand.length))
                        .error().redirect()
                        .output().processWith(br -> GraalVM.Version.of(br.lines()))
                        .run();
            } catch (Exception e) {
                throw new RuntimeException("Failed to get GraalVM version", e);
            }
        }
        return graalVMVersion;
    }

    public abstract boolean isContainer();

    public void setup(boolean processInheritIODisabled) {
    }

    public void build(List<String> args, String nativeImageName, String resultingExecutableName, Path outputDir,
            GraalVM.Version graalVMVersion, boolean debugSymbolsEnabled, boolean processInheritIODisabled)
            throws InterruptedException, IOException {
        preBuild(outputDir, args);
        try {
            final String[] buildCommand = getBuildCommand(outputDir, args);
            log.info(String.join(" ", buildCommand).replace("$", "\\$"));
            ProcessBuilder<Void> pb = ProcessBuilder.newBuilder(buildCommand[0])
                    .arguments(Arrays.copyOfRange(buildCommand, 1, buildCommand.length))
                    .directory(outputDir);
            pb.whileRunning(ph -> {
                if (!ph.isAlive()) {
                    return;
                }
                // todo: maybe have a "destroy-on-shutdown" switch?
                Thread hook = new Thread(() -> {
                    if (ph.supportsNormalTermination()) {
                        // ask nicely (not supported on Windows)
                        ph.destroy();
                    }
                    ph.waitUninterruptiblyFor(10, TimeUnit.SECONDS);
                    ProcessUtil.destroyAllForcibly(ph);
                }, "GraalVM terminator");
                Runtime.getRuntime().addShutdownHook(hook);
                try {
                    ph.waitUninterruptiblyFor();
                } finally {
                    Runtime.getRuntime().removeShutdownHook(hook);
                }
            });
            pb.output().consumeLinesWith(8192, System.out::println);
            // Why logOnSuccess(false) and then consumeWith? Because we get the stdErr twice otherwise.
            pb.error().logOnSuccess(false)
                    .consumeWith(br -> new ErrorReplacingProcessReader(br, outputDir.resolve("reports").toFile()).run());
            pb.run();
            boolean objcopyExists = objcopyExists();

            if (!debugSymbolsEnabled) {
                // Strip debug symbols even if not generated by GraalVM/Mandrel, because the underlying JDK might
                // contain them. Note, however, that starting with GraalVM/Mandrel 23.0 this is done by default when
                // generating debug info, so we don't want to do it twice and print twice a warning if objcopy is not
                // available.
                if (objcopyExists) {
                    objcopy(outputDir, "--strip-debug", resultingExecutableName);
                } else if (SystemUtils.IS_OS_LINUX) {
                    log.warn(
                            "objcopy executable not found in PATH. Debug symbols will therefore not be separated from the executable.");
                    log.warn("That also means that resulting native executable is larger as it embeds the debug symbols.");
                }
            }
        } finally {
            postBuild(outputDir, nativeImageName, resultingExecutableName);
        }
    }

    protected abstract String[] getGraalVMVersionCommand(List<String> args);

    protected abstract String[] getBuildCommand(Path outputDir, List<String> args);

    protected boolean objcopyExists() {
        return true;
    }

    protected abstract void objcopy(Path outputDir, String... args);

    protected void preBuild(Path outputDir, List<String> buildArgs) throws IOException, InterruptedException {
    }

    protected void postBuild(Path outputDir, String nativeImageName, String resultingExecutableName)
            throws InterruptedException, IOException {
    }

    /**
     * Run {@code command} in {@code workingDirectory} and log error if {@code errorMsg} is not null.
     *
     * @param command The command to run
     * @param errorMsg The error message to be printed in case of failure.
     *        If {@code null} the failure is ignored, but logged.
     * @param workingDirectory The directory in which to run the command
     */
    static void runCommand(String[] command, String errorMsg, File workingDirectory) {
        log.info(String.join(" ", command).replace("$", "\\$"));
        try {
            final ProcessBuilder<Void> pb = ProcessBuilder.newBuilder(command[0])
                    .arguments(Arrays.copyOfRange(command, 1, command.length));
            if (workingDirectory != null) {
                pb.directory(workingDirectory.toPath());
            }
            // Without logOnSuccess(false) the error stream is printed twice and with a "WARNING".
            pb.error().logOnSuccess(false);
            pb.run();
        } catch (Exception e) {
            if (errorMsg != null) {
                log.errorf(e, errorMsg);
            } else {
                log.debugf(e, "Command: " + String.join(" ", command) + " failed.");
            }
        }
    }

    /**
     * Run {@code command} and log error if {@code errorMsg} is not null.
     *
     * @param command
     * @param errorMsg
     */
    static void runCommand(String[] command, String errorMsg) {
        runCommand(command, errorMsg, null);
    }

    static class Result {
        private final int exitCode;

        public Result(int exitCode) {
            this.exitCode = exitCode;
        }

        public int getExitCode() {
            return exitCode;
        }

    }
}
